﻿using System;
using System.Collections.Generic;

namespace Project1
{
	/// <summary>
	/// Represents a two-dimensional grid environment.
	/// </summary>
    class GridEnvironment2D : IProblem
    {
		private GridCell[] grid;
		private Point2D startLocation;
		private Point2D goalLocation;

		#region Public properties
		/// <summary>
		/// Gets or sets the width of the grid.
		/// </summary>
		/// <value>
		/// The width of the grid.
		/// </value>
        public int Width { get; set; }

		/// <summary>
		/// Gets or sets the height of the grid.
		/// </summary>
		/// <value>
		/// The height of the grid.
		/// </value>
		public int Height { get; set; }
		#endregion

		#region Constructor
		/// <summary>
		/// Initializes a new instance of the <see cref="GridEnvironment2D"/> class.
		/// </summary>
		/// <param name="height">The height of the grid.</param>
		/// <param name="width">The width of the grid.</param>
		/// <param name="startLocation">The start location.</param>
		/// <param name="goldLocation">The location of the call containing the gold.</param>
		/// <param name="obstacles">The locations of the obstacles.</param>
		public GridEnvironment2D(int height, int width, Point2D startLocation, Point2D goldLocation, List<Point2D> obstacles)
        {
			//
            // TODO: Add validation
			// Store the grid defining values
			//
			this.startLocation = startLocation;
			this.goalLocation = goldLocation;
			this.Width = width;
			this.Height = height;
            //
			// Create a one-dimensional array to store the cells
			//
			grid = new GridCell[width * height];
			//
			// Populate it with cell objects
			//
            for (int index = 0; index < grid.Length; index++)
            {
				GridCell cell;
				//
				// Should this cell contain the gold?
				//
				if (ToIndex(goldLocation) == index)
				{
					cell = new GridCell(1, CellType.Gold);
				}
				//
				// Should this cell contain an obstacle?
				//
				else if (obstacles.Exists(x => ToIndex(x) == index))
				{
					cell = new GridCell(-1, CellType.Impassable);
				}
				else
				{
					//
					// Just an ordinary cell
					//
					cell = new GridCell(1, CellType.Normal);
				}
				grid[index] = cell;
            }
        }
		#endregion

		/// <summary>
		/// Gets the <see cref="Project1.GridCell"/> at the specified location in the grid.
		/// </summary>
		public GridCell this[Point2D point]
		{
			get
			{
				//
				// TODO: Add bounds check
				//
				return this.grid[ToIndex(point)];
			}
		}

		#region Private methods
		/// <summary>
		/// Converts two-dimensional coordinates into an index which can be used to find the corresponding 
		/// cell in the one-dimensional array.
		/// </summary>
		/// <param name="point">The location of the cell.</param>
		/// <returns>An index</returns>
		private int ToIndex(Point2D point)
		{
			return point.X + point.Y * this.Width;
		}

		/// <summary>
		/// Checks whether the supplied coordinates point to a location outside of the grid.
		/// </summary>
		/// <param name="location">The location coordinates.</param>
		/// <returns><c>true</c> if the location is out of bounds, otherwise <c>false</c>.</returns>
		private bool OutOfBounds(Point2D location)
		{
			return location.X < 0 || location.X >= this.Width || location.Y < 0 || location.Y >= this.Height;
		}
		#endregion

		#region ToString conversion
		/// <summary>
		/// Returns a <see cref="System.String"/> that represents this instance.
		/// </summary>
		/// <returns>
		/// A <see cref="System.String"/> that represents this instance.
		/// </returns>
		public override string ToString()
		{
			return ToStringImp(new Object[0]);
		}


		/// <summary>
		/// Returns a <see cref="System.String"/> that represents this instance.
		/// </summary>
		/// <param name="keys">An array with the coordinates of cells that should be specifically marked.</param>
		/// <returns>
		/// A <see cref="System.String"/> that represents this instance.
		/// </returns>
		/// <remarks>This method can be used to draw a solution path.</remarks>
		public string ToString(Object[] keys )
		{
			return ToStringImp( keys );
		}

		private string ToStringImp(Object[] keys)
		{
			string rowDivider = "-" + string.Join("----", new string[this.Width + 1]);
			string cellDivider = "|";
            string cellEnumerator = " ";
            for (int columnIndex = 0; columnIndex < this.Width; ++columnIndex)
            {
                string numberAsString = Convert.ToString(columnIndex);
                cellEnumerator += " " + numberAsString + string.Join(" ", new string[ 4 - numberAsString.Length ]);
            }

			string result = cellEnumerator + Environment.NewLine + rowDivider;
			for (int rowIndex = 0; rowIndex < this.Height; rowIndex++)
			{
                result += Environment.NewLine + cellDivider;
				for (int columnIndex = 0; columnIndex < this.Width; columnIndex++)
				{
					result += " " + GetLocationSymbol(keys, columnIndex, rowIndex) + " " + cellDivider;
				}
				result += " " + Convert.ToString(rowIndex) + Environment.NewLine + rowDivider;
			}
			return result;	
		}

		private string GetLocationSymbol( Object[] keys, int x, int y )
		{
			string pathSymbol = "P";
			string startLocationSymbol = "S";

			if (startLocation.X == x && startLocation.Y == y)
			{
				return startLocationSymbol;
			}
			
			if (this[new Point2D(x,y)].Type != CellType.Normal)  //To make sure that the path will not be drawn over obstacles or the goal
			{
				return this[new Point2D(x, y)].Type.ToString().Substring(0, 1);
			}
			
			foreach (Point2D pathPoint in keys)
			{
				if (pathPoint.X == x && pathPoint.Y == y)
				{
					return pathSymbol;
				}
			}

            return " ";
		}
		#endregion

		#region IProblem Members

		/// <summary>
		/// Gets the successors states reachable from the supplied <paramref name="state"/>.
		/// </summary>
		/// <param name="state">The state from which the successors should be generated.</param>
		/// <returns>
		/// A list of successor states.
		/// </returns>
		public IEnumerable<State> GetSuccessors(State state)
		{
			//
			// Find neighbour positions
			//
			Point2D currentLocation = (Point2D)state.Key;
			List<Point2D> neigbours = new List<Point2D>(4);
			neigbours.Add(new Point2D(currentLocation.X, currentLocation.Y - 1)); // UP
			neigbours.Add(new Point2D(currentLocation.X + 1, currentLocation.Y)); // RIGHT
			neigbours.Add(new Point2D(currentLocation.X, currentLocation.Y + 1)); // DOWN
			neigbours.Add(new Point2D(currentLocation.X - 1, currentLocation.Y)); // Left
			//
			// Eliminate unreachable cells
			//
			foreach (Point2D neighbour in neigbours)
			{
				if (OutOfBounds(neighbour))
				{
					continue;
				}
				GridCell cell = this[neighbour];
				if (cell.Type == CellType.Impassable)
				{
					continue;
				}
				yield return new State(neighbour, cell.Cost, state);
			}
		}

		/// <summary>
		/// Determines whether the specified state is a goal state.
		/// </summary>
		/// <param name="state">The state.</param>
		/// <returns>
		///   <c>true</c> if the specified state is a goal state; otherwise, <c>false</c>.
		/// </returns>
		public bool IsGoal(State state)
		{
			return this[(Point2D)state.Key].Type == CellType.Gold;
		}

		/// <summary>
		/// Gets the initial state.
		/// </summary>
		public State InitialState
		{
			get
			{
				return new State(startLocation, 0);
			}
		}

		/// <summary>
		/// Gets the goal state.
		/// </summary>
		/// <value>
		/// The goal state.
		/// </value>
		public State GoalState
		{
			get { return new State(goalLocation, 0); }
		}
		#endregion
	}
}
